# -*- coding: utf-8 -*-
from google.appengine.ext import db
from google.appengine.api import mail
from string import ascii_lowercase, digits
from random import choice
import datetime
import time
import hashlib

'''
La clase usuario tiene todo lo necesario para el registro, autenticación,
cambio de contraseña, reestablecimiento de contraseña, gestión de avatares
'''

class Usuario(db.Model):
    mail = db.EmailProperty(required=True)
    password = db.StringProperty(required=True, multiline=False, indexed=False)
    password_activacion = db.StringProperty(multiline=False)
    password_recuperar = db.StringProperty(multiline=False, indexed=False)
    password_recuperar_fecha = db.DateTimeProperty(required=True, default=datetime.datetime(datetime.MINYEAR, 1, 1))
    sexo = db.IntegerProperty(required=True,
                             choices=range(3),
                             default=0) #0:No contesta, 1:Hombre, 2:Mujer
    orientacion = db.IntegerProperty(required=True,
                                    choices=range(6),
                                    default=0) #0:No contesta, 1:Hetero, 2:Homo, 3:Bi, 4:Pan, 5:Poli
    desde = db.DateTimeProperty(required=True, auto_now_add=True, indexed=False)
    estado = db.IntegerProperty(required=True, choices=range(5), default=0) #0:No activo, 1:Activo, 2:Baneado, 3:Expulsado, 4:Administrador
    fin_baneo = db.DateTimeProperty(required=True, auto_now_add=True, indexed=False)
    enviar_mails = db.BooleanProperty(required=True, default=False, indexed=False)
    avatar = db.BlobProperty(required=True, default=None)
    recibir_anonimos = db.BooleanProperty(required=True, default=True, indexed=False)
    lista_ignorados = db.StringListProperty(required=True, default=[], indexed=False)
    ultima_visita_foro = db.DateTimeProperty(required=True, auto_now_add=True, indexed=False)
    polvos_enviados = db.IntegerProperty(required=True, default=0, indexed=False)
    mensajes_enviados = db.IntegerProperty(required=True, default=0, indexed=False)
    comentarios_enviados = db.IntegerProperty(required=True, default=0, indexed=False)
    
    def inicia_recuperar(self):
        '''
        Inicia el proceso de recuperar contraseña. Comprueba las fechas,
        establece el código y manda el correo. Si algo va mal devuelve el
        mensaje de error en el diccionario.
        '''
        if self.password_recuperar_fecha == datetime.datetime(datetime.MINYEAR, 1, 1) or (datetime.datetime.today() - self.password_recuperar_fecha).total_seconds() > 86400:
            # Podemos solicitar una nueva contraseña
            self.password_recuperar_fecha = datetime.datetime.today()
            p_r = self.generar_string_aleatorio(15)
            self.password_recuperar = p_r
            self.put()
            nombre = self.key().name()
            sender = "Administrador de geopolvazo <admin@geopolvazo.appspotmail.com>"
            to = "%s <%s>" % (nombre, self.mail)
            subject = "Reestablecimiento de password"
            body = u"¡Hola %s! Has solicitado reestablecer tu contraseña de Geopolvazo.\n\nPara ello entra en el enlace siguiente y sigue los pasos:\n\nhttp://geopolvazo.appspot.com/reestablecer/%s\n\nRecuerda que tienes una hora para realizar el paso desde el momento que recibiste el mail y solo puedes pedir un reestablecimiento de contraseña al día.\n\nEl administrador de Geopolvazo." % (nombre, p_r)
            body = body.encode("utf-8")
            m = mail.EmailMessage(sender=sender, subject=subject, to=to, body=body)
            m.send()
            return True
        else:
            return False
    
    @classmethod
    def buscar_por_password_recuperacion(cls, p_r):
        return cls.all().filter("password_recuperar =", p_r).get()
            
    @classmethod
    def generar_string_aleatorio(cls, lon):
        l = ascii_lowercase + digits
        return ''.join([choice(l) for _ in range(lon)])
    
    @classmethod
    def buscar_por_nombre(cls, n):
        '''
        Devuelve el usuario con el nombre aportado.
        Si no existe, devuelve None.
        '''
        return cls.get_by_key_name(n)
    
    @classmethod
    def buscar_por_mail(cls, m, key=False):
        '''
        Devuelve el usuario que tenga el mail m.
        Suponemos que el mail es unico
        Si key es True, solo devolveremos su clave
        '''
        return cls.all(keys_only=key).filter("mail =", m).get()
    
    @classmethod
    def ignorar(cls, usuario, ignorar, readmitir=False):
        '''
        Anyade a la lista (o quita de la lista) de ignorados del usuario u el
        usuario con nombre ign.
        Devolvemos True si la llamada tiene exito.
        '''
        u = cls.buscar_por_nombre(usuario)
        if u:
            if readmitir:
                if ignorar in u.lista_ignorados:
                    u.lista_ignorados.remove(ignorar)
                    u.put()
                    return True
            else:
                if not (ignorar in u.lista_ignorados):
                    u.lista_ignorados.append(ignorar)
                    u.put()
                    return True

    def cambiar_password(self, p, np):
        s_h = self.password.split(',')
        salt = s_h[0]
        if s_h[1] == hashlib.sha256(self.mail + p + salt).hexdigest():
            salt = self.generar_string_aleatorio(10)
            h = hashlib.sha256(self.mail + np + salt).hexdigest()
            self.password = '%s,%s' % (salt, h)
            self.put() 

    @classmethod
    def login_por_mail(cls, m, p):
        '''
        Comprueba la veracidad del usuario y contrasenya que pasamos.
        Devuelve un diccionario del tipo:
        'acceso' = True|False -> Indica el éxito de la operación
        Si el acceso es correcto, se deuelve:
          - 'nombre' -> String
          - 'admin' -> Bool
        Si el acceso es incorrecto, se devuelve:
        'motivo' = Motivo de la denegación de la entrada (Baneado, expulsado,
                 Cuenta sin comprobar...) No existirá si Acceso == True.
        'fecha' = En el caso que el usuario esté baneado, muestra la fecha de
                readmisión. Solo existirá si el usuario está baneado.
                
        Los motivos son: 1->No existe, 2->Contrasenya incorrecta, 3->Baneado,
                         4->Expulsado, 5->Cuenta no confirmada
        
        Siempre comprobamos la contrasenya antes de dar explicaciones sobre no
        poder entrar.
        '''
        u = cls.buscar_por_mail(m)
        if u:
            s_h = u.password.split(',')
            salt = s_h[0]
            if s_h[1] == hashlib.sha256(m + p + salt).hexdigest():
                # Contrasenya correcta
                if u.estado == 1: # Usuario activo
                    return {'acceso' : True,
                            'admin' : False,
                            'nombre' : u.key().name()} #Todo OK
                elif u.estado == 4: # Usuario administrador
                    return {'acceso' : True,
                            'admin' : True,
                            'nombre' : u.key().name()} #Todo OK y además admin
                elif u.estado == 3: # Usuario Expulsado
                    return {'acceso' : False, 'motivo' : 3} #Expulsado
                elif u.estado == 2: # Usuario Baneado
                    return {'acceso' : False, 'motivo' : 2,
                            'fecha' : int(time.mktime(u.fin_baneo.timetuple()))
                            } #Baneado
                else: # u.estado == 0: Sin confirmar
                    return {'acceso' : False, 'motivo' : 5} #No confirmada
            else:
                # Constrasenya incorrecta
                return {'acceso' : False, 'motivo' : 4} #Passwd incorrecta
        else:
            return {'acceso' : False, 'motivo' : 1} #No existe        
    
    @classmethod
    def activar_por_codigo_activacion(cls, pact):
        '''
        Devuelve el nombre de usuario de la base de datos con un determinado
        código de activación. Tras obtenerlo, le cambia el permiso a activado,
        borra el codigo de activacion y lo vuelve a insertar.
        Devuelve el nombre si se ha activado el usuario de forma correcta y
        None si no hay un usuario con dicha clave de activacion.
        '''
        u = cls.all().filter("password_activacion =", pact).get()
        if u:
            n = u.key().name()
            u.password_activacion = None #Poner el valor None borra el campo
            u.estado = 1
            u.put()
            return n
    
    @classmethod
    def insertar_usuario(cls, mail_usuario, password, nombre_usuario):
        '''
        Inserta un usuario en nuestra base de datos, suponiendo que los
        parametros sean unicos en nuestro datastore.
        En caso de que haya un usuario con el mismo nombre no lo inserta.
        No deberia haber mas de un usuario con el mismo mail... En teoria.
        Luego manda un mail con la direccion para confirmar la cuenta.
        '''
        salt = cls.generar_string_aleatorio(10)
        pact = cls.generar_string_aleatorio(10)
        h = hashlib.sha256(mail_usuario + password + salt).hexdigest()
        hash_password = '%s,%s' % (salt, h)
        cls.get_or_insert(key_name=nombre_usuario, mail=mail_usuario,
                          password_activacion=pact, password=hash_password,
                          avatar='89504E470D0A1A0A0000000D4948445200000032000000320802000000915D1FE6000000017352474200AECE1CE90000000467414D410000B18F0BFC6105000000097048597300000EC300000EC301C76FA86400000158494441545847ED96DB12C2200C44FBFF3F5DC7A93294CB5E24D4EAE093A321396C96C0B6DFF2B3DD926A5F584E5FFE42ADEDFC71F6EFC51A6A1D48297D22F40A6AD11216202858B5A23C8A63D1C2B984BCA0162161E1543F8C45252F767E915A0B4B33EF11E54E93802652BCD441FD705C849534A37B7845D2387D8BBD5429839E6AA9F5363EEDCFF388D0205DF966AAFA82A71555AC11B262AD988AABE51EA2F21AC9DE42C7008B51AB7E60E56F415A63AE5AC0374080FA2A8C570B8CA55EB1E6EF0A99D469A5533AC175583D2FEBACF67B8B4A058E6A0F8B0A16D3444B2D654C4CC4022FD251B5E8FA7CAAE1419AFF4BD312B5E8FA8FB170E618ACE6D8CC2F83E23BB5570056D343C058CA251B83550F11DAFD894D4CADF90256AF1DF8FC2B6AA13B5E19E24DF382859489DA2B6C9C5A636961296E38C78C7A6BDC46E009D9BC27A678CB17AE5CB1B01C0D975A4B2D470127F6A6DE7A000D19403CDEFD6D520000000049454E44AE426082'.decode('hex')
                          )
        sender = "Administrador de geopolvazo <admin@geopolvazo.appspotmail.com>"
        to = "%s <%s>" % (nombre_usuario, mail_usuario)
        subject = "El paso final para activar tu cuenta"
        body = u"¡Hola %s! Bienvenido/a a Geopolvazo.\n\nPara finalizar el registro solo te queda pulsar sobre el siguiente enlace y tu cuenta quedará activada:\n\nhttp://geopolvazo.appspot.com/activar/%s\n\nEntra cuando quieras, rellena tu perfil y disfruta de todo lo que ofrecemos.\n\nEl administrador de Geopolvazo." % (nombre_usuario, pact)
        body = body.encode("utf-8")
        m = mail.EmailMessage(sender=sender, subject=subject, to=to, body=body)
        m.send()
        
    @classmethod
    def estado_por_nombre(cls, n):
        '''
        Devuelve indicador de estado para el usuario con un nombre.
        Si no existe, devuelve None.
        Hay que tener en cuenta que puede devolver 0 (no activado) y seria
        equivalente a un valor False.
        '''
        u = cls.buscar_por_nombre(n)
        if u:
            return u.estado

    @classmethod
    def admin_por_nombre(cls, n):
        '''
        Devuelve True si un usuario tiene permisos de administrador.
        False si no existe o no es admin.
        '''
        u = cls.get_by_key_name(n)
        return u and u.estado == 4
        
    @classmethod
    def peticion_recuperar_password(cls, m):
        '''
        Inicia el proceso de peticion de cambio de contrasenya. Comprueba que
        cumple los requisitos y en su caso, manda un mail al usuario con el
        codigo de reestablecimiento.
        Devuelve un diccionario con el estado de la petición, codigos de error
        y otra info.
        
        - Para poder solicitar una petición de recuperar la contrasenya no se
          debe haber pedido otra en menos de una hora.
          
        motivo = 1: No se encuentra el mail en la base de datos.
        motivo = 2: Se ha pedido una recuperación hace menos de una hora.
        motivo = 3: La cuenta no se encuentra activa.
        '''
        u = cls.buscar_por_mail(m)
        if u:
            if u.estado == 1 or u.estado == 4:
                fecha_ahora = datetime.datetime.now()
                if (fecha_ahora - u.password_recuperar_fecha).total_seconds() > 3600: # Hace menos de una hora
                    prest = cls.generar_string_aleatorio(10)
                    u.password_recuperar = prest
                    u.password_recuperar_fecha = fecha_ahora
                    u.put()
                    sender = "Administrador de geopolvazo <admin@geopolvazo.appspotmail.com>"
                    nombre = u.key().name()
                    to = "%s <%s>" % (nombre, m)
                    subject = "Instrucciones para reestablecer tu password"
                    body = u"¡Hola, %s!\n\nSe ha recibido en Geopolvazo una petición para reestablecer tu contraseña. Si no has sido tú, por favor, ignora el mensaje.\n\nPara reestablecer tu contraseña pulsa sobre el siguiente enlace y sigue las instrucciones:\n\nhttp://geopolvazo.appspot.com/recuperar/%s\n\nRecuerda que desde el envío de este mensaje tienes una hora para reestablecerla.\n\nEl administrador de Geopolvazo." % (nombre, prest)
                    body = body.encode("utf-8")
                    m = mail.EmailMessage(sender=sender, subject=subject, to=to, body=body)
                    m.send()
                    return {'exito' : True}
                else: # Se ha pedido otra en menos de una hora
                    return {'exito' : False, 'motivo' : 2}
            else:
                return {'exito' : False, 'motivo' : 3}           
        else: # Error no existe
            return {'exito' : False, 'motivo' : 1}
            
    @classmethod
    def recuperar_password(cls, m, prest, npass):
        '''
        Finaliza el proceso de peticion de cambio de contrasenya.
        1) Compruba que el mail exista.
        2) Comprobamos que el usuario este activo
        3) Comprobamos que la clave se solicitase en menos de una hora
        4) Reestablecemos la contrasenya
        
        motivo = 1: El código de recuperacion de contraseña no coincide
        motivo = 2: El código de recuperación ha caducado
        motivo = 3: No se ha solicitado ninguna petición
        motivo = 4: La cuenta no está activo
        motivo = 5: La cuenta no existe
        '''
        u = cls.buscar_por_mail(m)
        if u: # Existe
            if u.estado == 1 or u.estado == 4: # Esta activo
                fecha_origen = datetime.datetime(datetime.MINYEAR, 1, 1)
                if u.password_recuperar_fecha != fecha_origen: # Tiene una recuperacion activa
                    if (datetime.datetime.now() - u.password_recuperar_fecha).total_seconds() < 3600: # No esta caducada
                        if prest == u.password_recuperar: # Las contrasenyas coinciden
                            salt = cls.generar_string_aleatorio(10)
                            h = hashlib.sha256(m + npass + salt).hexdigest()
                            u.password = '%s,%s' % (salt, h)
                            u.password_recuperar_fecha = fecha_origen
                            u.put()
                        else: # Las contrasenyas no coinciden
                            return {'exito' : False, 'motivo' : 1}
                    else: # El codigo de recuperacion de contrasenya ha caducado
                        return {'exito' : False, 'motivo' : 2}
                else:
                    return {'exito' : False, 'motivo' : 3}
            else:
                return {'exito' : False, 'motivo' : 4}
        else:
            return {'exito' : False, 'motivo' : 5}
    
    @classmethod
    def avatar_por_nombre(cls, n):
        '''
        Devuelve un avatar para un usuario.
        Si no existe el usuario, devuelve None.
        '''
        u = cls.buscar_por_nombre(n)
        if u:
            return u.avatar
        